Node-RED block in Snap4City Microservice library node-red-contrib-snap4city-d3-dashboard-widgets is "Snap4D3">
With this node you can add a custom D3 widget to an existing dashboard or a new one created by the node.
/**
*
* @param d3: instance of D3 library
* @param d3Data: data sent by node-red for rendering with D3.
* @param width : width of the widget display space
* @param height :height of the widget display space
* @param sendToNodeRed: function used to send back data from widget to node-red
* @returns D3 chart instance
*/
async function drawD3Chart(d3,d3Data,width,height,sendToNodeRed){
function BarChart(data, {
x = (d, i) => i, // given d in data, returns the (ordinal) x-value
y = d => d, // given d in data, returns the (quantitative) y-value
title, // given d in data, returns the title text
marginTop = 20, // the top margin, in pixels
marginRight = 0, // the right margin, in pixels
marginBottom = 30, // the bottom margin, in pixels
marginLeft = 40, // the left margin, in pixels
width = 640, // the outer width of the chart, in pixels
height = 400, // the outer height of the chart, in pixels
xDomain, // an array of (ordinal) x-values
xRange = [marginLeft, width - marginRight], // [left, right]
yType = d3.scaleLinear, // y-scale type
yDomain, // [ymin, ymax]
yRange = [height - marginBottom, marginTop], // [bottom, top]
xPadding = 0.1, // amount of x-range to reserve to separate bars
yFormat, // a format specifier string for the y-axis
yLabel, // a label for the y-axis
color = "currentColor" // bar fill color
} = {}) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
// Compute default domains, and unique the x-domain.
if (xDomain === undefined) {xDomain = X;}
if (yDomain === undefined) {yDomain = [0, d3.max(Y)];}
xDomain = new d3.InternSet(xDomain);
// Omit any data not present in the x-domain.
const I = d3.range(X.length).filter(i => xDomain.has(X[i]));
// Construct scales, axes, and formats.
const xScale = d3.scaleBand(xDomain, xRange).padding(xPadding);
const yScale = yType(yDomain, yRange);
const xAxis = d3.axisBottom(xScale).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 40, yFormat);
// Compute titles.
if (title === undefined) {
const formatValue = yScale.tickFormat(100, yFormat);
title = i => `${X[i]}\n${formatValue(Y[i])}`;
} else {
const O = d3.map(data, d => d);
const T = title;
title = i => T(O[i], i, data);
}
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(yAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(yLabel));
const bar = svg.append("g")
.attr("fill", color)
.selectAll("rect")
.data(I)
.join("rect")
.attr("x", i => xScale(X[i]))
.attr("y", i => yScale(Y[i]))
.attr("height", i => yScale(0) - yScale(Y[i]))
.attr("width", xScale.bandwidth())
.on("click", (event, index) => {
sendToNodeRed({x:X[index],y:Y[index]});
});
if (title) {bar.append("title")
.text(title);}
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis);
return svg.node(); //ATTENTION: THIS MUST BE RETURNED BY THE FUNCTION TO BE USED BY THE WIDGET
}
//ATTENTION: THIS BELOW MUST BE RETURNED BY THE FUNCTION TO BE USED BY THE WIDGET
return BarChart(d3Data, {
x: d => d.letter,
y: d => d.frequency,
xDomain: d3.groupSort(d3Data, ([d]) => -d.frequency, d => d.letter), // sort by descending frequency
yFormat: "%",
yLabel: "↑ Frequency",
width,
height,
color: "steelblue"
});
}
const d3Configuration=String.raw`
/**
*
* @param d3: instance of D3 library
* @param d3Data: data sent by node-red for rendering with D3.
* @param width : width of the widget display space
* @param height :height of the widget display space
* @param sendToNodeRed: function used to send back data from widget to node-red
* @returns D3 chart instance
*/
async function drawD3Chart(d3,d3Data,width,height,sendToNodeRed){
function BarChart(data, {
x = (d, i) => i, // given d in data, returns the (ordinal) x-value
y = d => d, // given d in data, returns the (quantitative) y-value
title, // given d in data, returns the title text
marginTop = 20, // the top margin, in pixels
marginRight = 0, // the right margin, in pixels
marginBottom = 30, // the bottom margin, in pixels
marginLeft = 40, // the left margin, in pixels
width = 640, // the outer width of the chart, in pixels
height = 400, // the outer height of the chart, in pixels
xDomain, // an array of (ordinal) x-values
xRange = [marginLeft, width - marginRight], // [left, right]
yType = d3.scaleLinear, // y-scale type
yDomain, // [ymin, ymax]
yRange = [height - marginBottom, marginTop], // [bottom, top]
xPadding = 0.1, // amount of x-range to reserve to separate bars
yFormat, // a format specifier string for the y-axis
yLabel, // a label for the y-axis
color = "currentColor" // bar fill color
} = {}) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
// Compute default domains, and unique the x-domain.
if (xDomain === undefined) {xDomain = X;}
if (yDomain === undefined) {yDomain = [0, d3.max(Y)];}
xDomain = new d3.InternSet(xDomain);
// Omit any data not present in the x-domain.
const I = d3.range(X.length).filter(i => xDomain.has(X[i]));
// Construct scales, axes, and formats.
const xScale = d3.scaleBand(xDomain, xRange).padding(xPadding);
const yScale = yType(yDomain, yRange);
const xAxis = d3.axisBottom(xScale).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 40, yFormat);
// Compute titles.
if (title === undefined) {
const formatValue = yScale.tickFormat(100, yFormat);
title = i => X[i]+"\n"+formatValue(Y[i]);
} else {
const O = d3.map(data, d => d);
const T = title;
title = i => T(O[i], i, data);
}
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
svg.append("g")
.attr("transform", "translate("+marginLeft+",0)")
.call(yAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(yLabel));
const bar = svg.append("g")
.attr("fill", color)
.selectAll("rect")
.data(I)
.join("rect")
.attr("x", i => xScale(X[i]))
.attr("y", i => yScale(Y[i]))
.attr("height", i => yScale(0) - yScale(Y[i]))
.attr("width", xScale.bandwidth())
.on("click", (event, index) => {
sendToNodeRed({x:X[index],y:Y[index]});
});
if (title) {bar.append("title")
.text(title);}
svg.append("g")
.attr("transform", "translate(0,"+(height - marginBottom)+")")
.call(xAxis);
return svg.node(); //ATTENTION: THIS MUST BE RETURNED BY THE FUNCTION TO BE USED BY THE WIDGET
}
//ATTENTION: THIS BELOW MUST BE RETURNED BY THE FUNCTION TO BE USED BY THE WIDGET
return BarChart(d3Data, {
x: d => d.letter,
y: d => d.frequency,
xDomain: d3.groupSort(d3Data, ([d]) => -d.frequency, d => d.letter), // sort by descending frequency
yFormat: "%",
yLabel: "↑ Frequency",
width,
height,
color: "steelblue"
});
}
`;
msg["configuration"]=d3Configuration;
return msg;